Skip to content

sqlite: add incremental blob I/O - #65444

Open
arimxyer wants to merge 2 commits into
nodejs:mainfrom
arimxyer:sqlite-incremental-blob-io
Open

sqlite: add incremental blob I/O#65444
arimxyer wants to merge 2 commits into
nodejs:mainfrom
arimxyer:sqlite-incremental-blob-io

Conversation

@arimxyer

@arimxyer arimxyer commented Aug 20, 2026

Copy link
Copy Markdown

Add database.openBlob(), which returns a BlobHandle for reading and writing ranges of a single BLOB or TEXT value without materializing it. It wraps sqlite3_blob_open() and the related C API.

BlobHandle exposes byteLength, read(), write(), reopen(), close(), and [Symbol.dispose]. There is no JavaScript implementation layer; lib/sqlite.js re-exports the binding.

The public node:sqlite JavaScript API currently materializes a whole value and has no partial-write path. A custom loadable SQLite extension can reach sqlite3_blob_open() on the existing connection, so this is possible outside core with native code. The proposed API makes SQLite's primitive available to ordinary JavaScript without an extension-specific bridge or a second connection.

Implementation notes

Options are read before state is captured. Reading an options property can run user code. Each operation reads its options, rechecks the handle and database, and only then captures the current buffer length, backing-store pointer, and sqlite3_blob_bytes(). This covers accessors that detach or resize the buffer or close the handle/database during argument processing.

Close reports only a new deferred error. sqlite3_blob_close() finalizes the internal statement and can repeat its last result code. The handle remembers the last code, so close does not report an error already thrown by a failed operation. A new close-time commit failure is propagated after the handle is released.

Disposal propagates close failures deliberately. BlobHandle disposal and DatabaseSync disposal are idempotent when already closed, but otherwise they do not suppress close errors. Either can close the last writable blob handle and therefore be a commit boundary. Suppressing that failure would make a using scope appear successful after SQLite rolled its writes back. Session disposal is left unchanged because its separate reentrancy-error behavior is outside this pull request.

Incremental writes are raw-byte operations. They do not execute SQL UPDATE, run triggers, or re-check constraints, and arbitrary writes to TEXT can create invalid UTF-8. The documentation calls out those SQLite semantics, the writable-handle transaction lifetime, and the generated-column and virtual-table restrictions.

The second commit fixes a pre-existing error-helper bug exposed by the new database-close path: THROW_ERR_SQLITE_ERROR(isolate, errcode) selected an overload that left errstr empty. It remains separate and can be lifted into its own pull request if preferred.

Tests

test/parallel/test-sqlite-blob.js contains 48 tests covering API options, range and integer validation, buffer detachment and resizing, reentrancy, authorizer callbacks, object lifetimes, SQLite restrictions, raw-byte write semantics, large chunked transfers, and explicit and disposal close failures. The blocked-commit cases verify through a fresh connection that the failed commit rolled back while the exception remained observable.

On one Linux machine, reading a 512 MiB value from disk as a whole took about 100 ms and about 600 MB RSS; reading it in 64 KiB chunks through a handle took about 40 ms and about 76 MB RSS. This is a single illustrative measurement, not a crossover study. The peak-memory difference is the motivation; the timing is incidental.

Fixes: #65445 Refs: https://sqlite.org/c3ref/blob_open.html


Assisted-by: frontier coding agent (opus-5)

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/sqlite

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels Aug 20, 2026
@arimxyer
arimxyer force-pushed the sqlite-incremental-blob-io branch from 9feff52 to e8aa961 Compare August 20, 2026 22:00
@trivikr trivikr added the sqlite Issues and PRs related to the SQLite subsystem. label Aug 20, 2026
@arimxyer

Copy link
Copy Markdown
Author

The branch has been updated since this was opened. What changed:

  • blobHandle[Symbol.dispose]() and database[Symbol.dispose]() now propagate close errors instead of suppressing them. Disposal can be the commit point for a writable handle, so suppressing meant a using scope could report success after SQLite had rolled the write back.
  • Corrected the documented expired/aborted handle semantics, including what byteLength reports on each path, and that a successful reopen() can recover an expired handle.
  • Documented that incremental writes are raw-byte operations: they do not run triggers or re-check constraints, and writing TEXT can produce invalid UTF-8. Added the generated-column and virtual-table restrictions.
  • Tests are now 48. The full test-sqlite-* suite and make lint-cpp lint-md pass locally.

Could a collaborator start CI when convenient? This is my first PR here, so it needs the Actions approval in addition to request-ci. No rush, and thanks.

node:sqlite has a single BLOB boundary: a whole SQLite value becomes a
whole Uint8Array, on read and on write. Reading one byte of a large
value therefore allocates all of it, and there is no partial write path
at all.

Expose the SQLite incremental blob interface as database.openBlob(),
returning a BlobHandle with byteLength, read(), write(), reopen(),
close(), and Symbol.dispose. Keep the surface to the primitive so that
streams, crypto, and compression can compose on top in userland.

The public node:sqlite JavaScript API does not expose its sqlite3*
handle. A custom loadable SQLite extension can use the existing
connection, but absent such native code a userland implementation needs
a second connection.

Reading an options property can run user code. Each operation reads its
options first, rechecks state, and only then captures the buffer size,
buffer pointer, and blob size. This prevents accessors from invalidating
state or detaching or resizing a buffer after validation.

Handle operations are barred from authorizer callbacks. Identifiers
containing NUL are rejected, and zero-length transfers still reach
SQLite so its range, aborted-handle, and write-permission checks apply.

Closing a writable handle can commit and report an error deferred from
an earlier write. A close does not repeat an error already delivered,
but it propagates a new commit failure after releasing the handle.

BlobHandle disposal deliberately propagates close errors. DatabaseSync
disposal does too because this change gives database close a new failure
path through owned writable handles. Suppressing either failure would
report successful disposal after SQLite rolled back the transaction.
Session disposal remains unchanged because its separate error behavior
is outside the scope of this change. Closed-state checks preserve
idempotence.

Refs: https://sqlite.org/c3ref/blob_open.html

Signed-off-by: Ari Mayer <ari111097@gmail.com>
The int overload of THROW_ERR_SQLITE_ERROR() sets errcode on the error
it throws but not errstr, because it passes the string it just looked up
as the message and then calls the const char* overload of
CreateSQLiteError(), which takes that as a plain message and leaves the
errstr slot empty. The int overload of CreateSQLiteError() already sets
both, so call that instead.

Every other error raised by node:sqlite carries errstr, and the tests
for StatementSync already assert it, so the two paths reaching this
helper -- applyChangeset() and a close() that reports a blob commit --
were the only ones where it was missing.

Signed-off-by: Ari Mayer <ari111097@gmail.com>
@arimxyer
arimxyer force-pushed the sqlite-incremental-blob-io branch from e8aa961 to 706eb9d Compare August 21, 2026 17:37
@arimxyer
arimxyer marked this pull request as ready for review August 21, 2026 17:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. sqlite Issues and PRs related to the SQLite subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sqlite: incremental BLOB I/O via sqlite3_blob_open()

3 participants